package scaladuct

import asm.{JavaBean, Scala2JavaBean}
import net.sf.cglib.beans.BeanMap
import scala.collection.mutable.Map
import java.lang.annotation.Annotation
import java.lang.{Class, String}
import org.apache.commons.convert.Converters
import java.util.HashMap
import java.lang.reflect.{MalformedParameterizedTypeException, Type, Method}

/**
 * <p>
 * Created by IntelliJ IDEA.
 * User: zslajchrt
 * Date: Aug 15, 2010
 * Time: 5:01:44 PM
 */

object BeanUtils {
  def createBeanInfo(bean: AnyRef): BeanInfo = {
    if (bean.isInstanceOf[java.util.Map[String, AnyRef]]) {
      new MapBeanInfo(bean.asInstanceOf[java.util.Map[String, AnyRef]])
    } else if (bean.isInstanceOf[ScalaObject] && !isJavaBeanAnnotated(bean)) {
      val javaBean: AnyRef = Scala2JavaBean(bean)
      new JavaBeanInfo(javaBean)
    } else
      new JavaBeanInfo(bean)
  }

  private def isJavaBeanAnnotated(bean: AnyRef): Boolean = {

    def hasJavaBeanAnnotation(cls: Class[_]): Boolean = {
      if (cls == classOf[Object])
        false
      else if (cls.getAnnotation(classOf[JavaBean]) != null)
        true
      else
        hasJavaBeanAnnotation(cls.getSuperclass)
    }

    hasJavaBeanAnnotation(bean.getClass)
  }

  def convertPropValuesToString(props: List[PropertyValue[AnyRef]]): java.util.Map[String, String] = {
    val map = new HashMap[String, String]()
    props.foreach(prop => {
      if (prop.value == null)
        map.put(prop.name, null)
      else
        map.put(prop.name, convertValueToString(prop))
    })
    map
  }

  private def convertValueToString[T](prop: PropertyValue[AnyRef]): String = {
    val converter = Converters.getConverter(
      prop.clazz.asInstanceOf[Class[AnyRef]],
      classOf[String]);
    converter.convert(prop.value)
  }

  def createClone[X <: AnyRef](original: X): X = {
    val clazz: Class[X] = original.getClass().asInstanceOf[Class[X]]
    val clone: X = clazz.newInstance
    copyBean(original, clone)
    clone
  }

  def copyBean[X <: AnyRef](source: X, target: X) {
    val sourceBeanInfo = createBeanInfo(source)
    val targetBeanInfo = createBeanInfo(target)
    sourceBeanInfo.asMap.keys.foreach(propName => sourceBeanInfo.get(propName) match {
      case Some(value) => targetBeanInfo.put(propName, value)
      case None =>
    })
  }

  def updateBean[X <: AnyRef](sourceBeanInfo: BeanInfo, target: X) {
    val targetBeanInfo = createBeanInfo(target)
    updateBean(sourceBeanInfo.asMap, target)
  }

  def updateBean[X <: AnyRef](sourceMap: java.util.Map[String, AnyRef], target: X) {
    val scalaMap = scala.collection.JavaConversions.JMapWrapper(sourceMap).asInstanceOf[Map[String, AnyRef]]
    updateBean(scalaMap, target)
  }

  def updateBean[X <: AnyRef](sourceMap: Map[String, AnyRef], target: X) {
    val targetBeanInfo = createBeanInfo(target)
    sourceMap.keys.foreach(propName => sourceMap.get(propName) match {
      case Some(value) => targetBeanInfo.put(propName, value)
      case None =>
    })
  }

  def updateSinglePropertyBean(bean: AnyRef, value: AnyRef) {
    val beanInfo = createBeanInfo(bean)
    require(beanInfo.asMap.size == 1, "bean must have only one property: " + beanInfo.asMap.size)
    val propertyName = beanInfo.asMap.head._1
    beanInfo.put(propertyName, value)
  }
}

trait BeanInfo {
  def asMap: Map[String, AnyRef]

  def asJavaMap: java.util.Map[String, AnyRef]

  def propertyType(propertyName: String): Class[_]

  def propertyAnnotation[A <: java.lang.annotation.Annotation](propertyName: String, annotationClass: Class[A]): A

  def propertyAnnotations(propertyName: String): List[java.lang.annotation.Annotation]

  def put(propertyName: String, value: AnyRef): Unit

  def get(propertyName: String): Option[AnyRef]

  def propertyGenericType(propertyName: String): Type

}

@serializable
@SerialVersionUID(1L)
class JavaBeanInfo(val bean: AnyRef) extends BeanInfo {
  @transient
  private var map: Map[String, AnyRef] = null
  @transient
  private var bMap: BeanMap = null

  def beanMap: BeanMap = if (bMap != null) bMap else {
    bMap = BeanMap.create(bean)
    bMap
  }

  def asJavaMap = beanMap.asInstanceOf[java.util.Map[String, AnyRef]]

  def asMap: Map[String, AnyRef] = if (map != null) map else {
    map = scala.collection.JavaConversions.JMapWrapper(beanMap).asInstanceOf[Map[String, AnyRef]]
    map
  }

  def propertyType(propertyName: String) = beanMap.getPropertyType(propertyName)

  def put(propertyName: String, value: AnyRef) = asMap.put(propertyName, value)

  def get(propertyName: String) = asMap.get(propertyName)

  def propertyAnnotation[A <: Annotation](propertyName: String, annotationClass: Class[A]) = {
    if (Scala2JavaBean.isConvertedScalaBean(bean))
      Scala2JavaBean.getConvertedScalaBeanPropertyAnnotation(propertyName, bean, annotationClass)
    else
      findGetter(propertyName).getAnnotation(annotationClass)
  }

  def propertyAnnotations(propertyName: String): List[java.lang.annotation.Annotation] = {
    if (Scala2JavaBean.isConvertedScalaBean(bean))
      Scala2JavaBean.getConvertedScalaBeanPropertyAnnotations(propertyName, bean)
    else
      findGetter(propertyName).getAnnotations().toList
  }

  private def findGetter(propertyName: String): Method = {
    val capitalizedName: String = getterName(propertyName)
    try {
      bean.getClass.getMethod("get" + capitalizedName)
    }
    catch {
      case e: NoSuchMethodException => {
        bean.getClass.getMethod("is" + capitalizedName)
      }
    }
  }

  private def getterName[A <: Annotation](propertyName: String): String = {
    propertyName.charAt(0).toUpper + propertyName.substring(1)
  }


  def propertyGenericType(propertyName: String): Type = {
    val getter: Method = findGetter(propertyName)
    try {
      getter.getGenericReturnType
    }
    catch {
      case e: MalformedParameterizedTypeException => {
         throw e
      }
    }
  }
}

class MapBeanInfo(val map: java.util.Map[String, AnyRef]) extends BeanInfo {
  def propertyGenericType(propertyName: String) = classOf[AnyRef]

  def get(propertyName: String) = if (map.containsKey(propertyName))
    Some(map.get(propertyName))
  else
    None

  def put(propertyName: String, value: AnyRef) = map.put(propertyName, value)

  def propertyAnnotations(propertyName: String) = Nil

  def propertyAnnotation[A <: Annotation](propertyName: String, annotationClass: Class[A]): A = null.asInstanceOf[A]

  def propertyType(propertyName: String) = classOf[AnyRef]

  def asJavaMap = map

  def asMap = scala.collection.JavaConversions.JMapWrapper(asJavaMap).asInstanceOf[Map[String, AnyRef]]
}